"""
写入 Excel
"""

import re
from re import Pattern

from xlsxwriter import Workbook
from xlsxwriter.format import Format
from xlsxwriter.worksheet import Worksheet

import utils.enum_utils as enum
from my_typing import *
from utils.data_utils import ExcelHeaderInfo


class ExcelWriter:
    """ Excel 写入器 """

    ENGLISH_BYTE_LENGTH: int = 1
    """ 单个英文字符的字节长度（UTF-8 编码中） """

    CHINESE_BYTE_LENGTH: int = 3
    """ 单个中文字符的字节长度（UTF-8 编码中） """

    def __init__(
        self,
        file_name: StrOrBytesIO,
        header_info_list: list[ExcelHeaderInfo],
        data_list: list[dict[str, AnyOrNone]],
        border: int = 0,
        font_size: int = 12,
        line_height: float = 1.5,
        freeze_first_row: bool = True,
        freeze_first_col: bool = True,
        header_align: Literal["left", "center", "right"] = enum.ExcelTextAlign.CENTER.value,
        content_align: Literal["left", "center", "right"] = enum.ExcelTextAlign.CENTER.value,
    ) -> None:
        """
        Args:
            file_name: 文件名/文件流
            header_info_list: 标题信息列表
            data_list: 数据映射列表
            border: 边框粗细（像素）
            font_size: 字号大小
            line_height: 行高
            freeze_first_row: 是否冻结首行
            freeze_first_col: 是否冻结首列
            header_align: 标题对齐方式（left.左对齐 / center.居中 / right.右对齐）
            content_align: 内容对齐方式（left.左对齐 / center.居中 / right.右对齐）

        Raises:
            ValueError 1: 标题长度与内容长度不符
            ValueError 2: 标题 Key 与内容 Key 不一致
        """

        self.header_en_2_cn: DictStrStr = {
            header_info.name: header_info.label
            for header_info in header_info_list
        }
        """ 标题名称映射（英文标题-中文标题） """

        self.header_cn_2_type: dict[str, Callable] = {
            header_info.label: header_info.type
            for header_info in header_info_list
        }
        """ 标题类型映射（中文标题-数据类型） """

        if len(self.header_en_2_cn) != len(data_list[0]):
            raise ValueError("标题长度与内容长度不符")

        if set(self.header_en_2_cn) != set(data_list[0]):
            raise ValueError("标题 Key 与内容 Key 不一致")

        self.font_size: int = font_size
        """ 字号大小 """

        self.line_height: int = int(font_size * line_height)
        """ 行高（字号 * 输入的行高） """

        self.default_sheet_name: str = "Sheet1"
        """ 默认 Sheet 页 """

        self.freeze_first_row: bool = freeze_first_row
        """ 是否冻结首行 """

        self.freeze_first_col: bool = freeze_first_col
        """ 是否冻结首列 """

        self.data_list: list[DictStrAny] = data_list
        """ 数据映射列表 """

        self.base_format: dict[str, IntOrStr] = {
            "border": border,
            "font_size": font_size,
            "valign": "vcenter",  # 垂直居中
        }
        """ 基本单元格格式 """

        self.header_format: dict[str, IntOrStr] = {
            **self.base_format,
            "align": header_align,
            "bold": True,
        }
        """ 表头单元格格式（默认为`粗体`） """

        self.content_format: dict[str, IntOrStr] = {
            **self.base_format,
            "align": content_align,
        }
        """ 内容单元格格式 """

        self.file_name: StrOrBytesIO = file_name
        """ Excel 文件 """

    @classmethod
    def _get_content_length(cls, content: str) -> int:
        """
        获取内容的长度（单个中文字符长度算作 3）

        Args:
            content: 内容

        Returns:
            内容的长度
        """

        pattern: Pattern[str] = re.compile(r"[\u4e00-\u9fa5]+")
        """ 匹配中文字符的正则 """

        return sum(
            (
                cls.CHINESE_BYTE_LENGTH
                if pattern.search(i)
                else cls.ENGLISH_BYTE_LENGTH
            )
            for i in content
        )

    def write(self) -> None:
        """ 将数据写入 Excel """

        work_book: Workbook = Workbook(self.file_name)
        """ Excel 工作簿对象 """

        work_sheet: Worksheet = work_book.add_worksheet(self.default_sheet_name)
        """ 工作表 Sheet 页 """

        content_cell_format: Format = work_book.add_format(self.content_format)
        """ 内容单元格格式 """

        # 写入表头 & 表头单元格格式
        work_sheet.write_row(
            row=0,
            col=0,
            data=self.header_en_2_cn.values(),
            cell_format=work_book.add_format(self.header_format),
        )

        # 设置表头行高
        work_sheet.set_row(
            row=0,
            height=self.line_height,
        )

        # 冻结首行/首列
        if (
            self.freeze_first_row
            or self.freeze_first_col
        ):
            work_sheet.freeze_panes(
                row=int(self.freeze_first_row),
                col=int(self.freeze_first_col),
            )

        column_2_widths: dict[int, SetInt] = {}
        """
        自适应列宽（列宽 = 当前列的内容的最大长度）

        - Key:   列索引
        - Value: 当且列所有有数据的单元格的内容宽度集合（包括表头）

        >>> {
        ...     0: {3, 5, 4, 8},
        ...     2: {1, 7, 5},
        ... }
        """

        for row, data_dic in enumerate(self.data_list, 1):
            # 设置内容单元格行高
            work_sheet.set_row(
                row=row,
                height=self.line_height,
            )

            for column, (key, cn_label) in enumerate(self.header_en_2_cn.items()):
                value = data_dic[key]
                converter: Callable = self.header_cn_2_type[cn_label]
                """ 类型转换器（转换成指定的类型） """

                write_value: Any = converter(value)
                column_2_widths.setdefault(column, set()).update(
                    (
                        self._get_content_length(str(write_value)),
                        self._get_content_length(cn_label),
                    )
                )
                # 写入内容单元格数据
                work_sheet.write_row(
                    row=row,
                    col=column,
                    data=(write_value,),
                    cell_format=content_cell_format,
                )

        for column, widths in column_2_widths.items():
            # 设置单元格列宽
            work_sheet.set_column(
                first_col=column,
                last_col=column,
                width=max(widths),
            )

        # 关闭 `Excel 工作簿对象`
        work_book.close()
